import Cartesian3 from "../Core/Cartesian3.js";
import Cartographic from "../Core/Cartographic.js";
import defaultValue from "../Core/defaultValue.js";
import defined from "../Core/defined.js";
import DeveloperError from "../Core/DeveloperError.js";
import Event from "../Core/Event.js";
import getTimestamp from "../Core/getTimestamp.js";
import CesiumMath from "../Core/Math.js";
import Matrix4 from "../Core/Matrix4.js";
import OrthographicFrustum from "../Core/OrthographicFrustum.js";
import OrthographicOffCenterFrustum from "../Core/OrthographicOffCenterFrustum.js";
import Ray from "../Core/Ray.js";
import Rectangle from "../Core/Rectangle.js";
import Visibility from "../Core/Visibility.js";
import QuadtreeOccluders from "./QuadtreeOccluders.js";
import QuadtreeTile from "./QuadtreeTile.js";
import QuadtreeTileLoadState from "./QuadtreeTileLoadState.js";
import SceneMode from "./SceneMode.js";
import TileReplacementQueue from "./TileReplacementQueue.js";
import TileSelectionResult from "./TileSelectionResult.js";

/**
 * Renders massive sets of data by utilizing level-of-detail and culling.  The globe surface is divided into
 * a quadtree of tiles with large, low-detail tiles at the root and small, high-detail tiles at the leaves.
 * The set of tiles to render is selected by projecting an estimate of the geometric error in a tile onto
 * the screen to estimate screen-space error, in pixels, which must be below a user-specified threshold.
 * The actual content of the tiles is arbitrary and is specified using a {@link QuadtreeTileProvider}.
 *
 * @alias QuadtreePrimitive
 * @constructor
 * @private
 *
 * @param {QuadtreeTileProvider} options.tileProvider The tile provider that loads, renders, and estimates
 *        the distance to individual tiles.
 * @param {Number} [options.maximumScreenSpaceError=2] The maximum screen-space error, in pixels, that is allowed.
 *        A higher maximum error will render fewer tiles and improve performance, while a lower
 *        value will improve visual quality.
 * @param {Number} [options.tileCacheSize=100] The maximum number of tiles that will be retained in the tile cache.
 *        Note that tiles will never be unloaded if they were used for rendering the last
 *        frame, so the actual number of resident tiles may be higher.  The value of
 *        this property will not affect visual quality.
 */
function QuadtreePrimitive(options) {
  //>>includeStart('debug', pragmas.debug);
  if (!defined(options) || !defined(options.tileProvider)) {
    throw new DeveloperError("options.tileProvider is required.");
  }
  if (defined(options.tileProvider.quadtree)) {
    throw new DeveloperError(
      "A QuadtreeTileProvider can only be used with a single QuadtreePrimitive"
    );
  }
  //>>includeEnd('debug');

  this._tileProvider = options.tileProvider;
  this._tileProvider.quadtree = this;

  this._debug = {
    enableDebugOutput: false,

    maxDepth: 0,
    maxDepthVisited: 0,
    tilesVisited: 0,
    tilesCulled: 0,
    tilesRendered: 0,
    tilesWaitingForChildren: 0,

    lastMaxDepth: -1,
    lastMaxDepthVisited: -1,
    lastTilesVisited: -1,
    lastTilesCulled: -1,
    lastTilesRendered: -1,
    lastTilesWaitingForChildren: -1,

    suspendLodUpdate: false,
  };

  var tilingScheme = this._tileProvider.tilingScheme;
  var ellipsoid = tilingScheme.ellipsoid;

  this._tilesToRender = [];
  this._tileLoadQueueHigh = []; // high priority tiles are preventing refinement
  this._tileLoadQueueMedium = []; // medium priority tiles are being rendered
  this._tileLoadQueueLow = []; // low priority tiles were refined past or are non-visible parts of quads.
  this._tileReplacementQueue = new TileReplacementQueue();
  this._levelZeroTiles = undefined;
  this._loadQueueTimeSlice = 5.0;
  this._tilesInvalidated = false;

  this._addHeightCallbacks = [];
  this._removeHeightCallbacks = [];

  this._tileToUpdateHeights = [];
  this._lastTileIndex = 0;
  this._updateHeightsTimeSlice = 2.0;

  // If a culled tile contains _cameraPositionCartographic or _cameraReferenceFrameOriginCartographic, it will be marked
  // TileSelectionResult.CULLED_BUT_NEEDED and added to the list of tiles to update heights,
  // even though it is not rendered.
  // These are updated each frame in `selectTilesForRendering`.
  this._cameraPositionCartographic = undefined;
  this._cameraReferenceFrameOriginCartographic = undefined;

  /**
   * Gets or sets the maximum screen-space error, in pixels, that is allowed.
   * A higher maximum error will render fewer tiles and improve performance, while a lower
   * value will improve visual quality.
   * @type {Number}
   * @default 2
   */
  this.maximumScreenSpaceError = defaultValue(
    options.maximumScreenSpaceError,
    2
  );

  /**
   * Gets or sets the maximum number of tiles that will be retained in the tile cache.
   * Note that tiles will never be unloaded if they were used for rendering the last
   * frame, so the actual number of resident tiles may be higher.  The value of
   * this property will not affect visual quality.
   * @type {Number}
   * @default 100
   */
  this.tileCacheSize = defaultValue(options.tileCacheSize, 100);

  /**
   * Gets or sets the number of loading descendant tiles that is considered "too many".
   * If a tile has too many loading descendants, that tile will be loaded and rendered before any of
   * its descendants are loaded and rendered. This means more feedback for the user that something
   * is happening at the cost of a longer overall load time. Setting this to 0 will cause each
   * tile level to be loaded successively, significantly increasing load time. Setting it to a large
   * number (e.g. 1000) will minimize the number of tiles that are loaded but tend to make
   * detail appear all at once after a long wait.
   * @type {Number}
   * @default 20
   */
  this.loadingDescendantLimit = 20;

  /**
   * Gets or sets a value indicating whether the ancestors of rendered tiles should be preloaded.
   * Setting this to true optimizes the zoom-out experience and provides more detail in
   * newly-exposed areas when panning. The down side is that it requires loading more tiles.
   * @type {Boolean}
   * @default true
   */
  this.preloadAncestors = true;

  /**
   * Gets or sets a value indicating whether the siblings of rendered tiles should be preloaded.
   * Setting this to true causes tiles with the same parent as a rendered tile to be loaded, even
   * if they are culled. Setting this to true may provide a better panning experience at the
   * cost of loading more tiles.
   * @type {Boolean}
   * @default false
   */
  this.preloadSiblings = false;

  this._occluders = new QuadtreeOccluders({
    ellipsoid: ellipsoid,
  });

  this._tileLoadProgressEvent = new Event();
  this._lastTileLoadQueueLength = 0;

  this._lastSelectionFrameNumber = undefined;
}

Object.defineProperties(QuadtreePrimitive.prototype, {
  /**
   * Gets the provider of {@link QuadtreeTile} instances for this quadtree.
   * @type {QuadtreeTile}
   * @memberof QuadtreePrimitive.prototype
   */
  tileProvider: {
    get: function () {
      return this._tileProvider;
    },
  },
  /**
   * Gets an event that's raised when the length of the tile load queue has changed since the last render frame.  When the load queue is empty,
   * all terrain and imagery for the current view have been loaded.  The event passes the new length of the tile load queue.
   *
   * @memberof QuadtreePrimitive.prototype
   * @type {Event}
   */
  tileLoadProgressEvent: {
    get: function () {
      return this._tileLoadProgressEvent;
    },
  },

  occluders: {
    get: function () {
      return this._occluders;
    },
  },
});

/**
 * Invalidates and frees all the tiles in the quadtree.  The tiles must be reloaded
 * before they can be displayed.
 *
 * @memberof QuadtreePrimitive
 */
QuadtreePrimitive.prototype.invalidateAllTiles = function () {
  this._tilesInvalidated = true;
};

function invalidateAllTiles(primitive) {
  // Clear the replacement queue
  var replacementQueue = primitive._tileReplacementQueue;
  replacementQueue.head = undefined;
  replacementQueue.tail = undefined;
  replacementQueue.count = 0;

  clearTileLoadQueue(primitive);

  // Free and recreate the level zero tiles.
  var levelZeroTiles = primitive._levelZeroTiles;
  if (defined(levelZeroTiles)) {
    for (var i = 0; i < levelZeroTiles.length; ++i) {
      var tile = levelZeroTiles[i];
      var customData = tile.customData;
      var customDataLength = customData.length;

      for (var j = 0; j < customDataLength; ++j) {
        var data = customData[j];
        data.level = 0;
        primitive._addHeightCallbacks.push(data);
      }

      levelZeroTiles[i].freeResources();
    }
  }

  primitive._levelZeroTiles = undefined;

  primitive._tileProvider.cancelReprojections();
}

/**
 * Invokes a specified function for each {@link QuadtreeTile} that is partially
 * or completely loaded.
 *
 * @param {Function} tileFunction The function to invoke for each loaded tile.  The
 *        function is passed a reference to the tile as its only parameter.
 */
QuadtreePrimitive.prototype.forEachLoadedTile = function (tileFunction) {
  var tile = this._tileReplacementQueue.head;
  while (defined(tile)) {
    if (tile.state !== QuadtreeTileLoadState.START) {
      tileFunction(tile);
    }
    tile = tile.replacementNext;
  }
};

/**
 * Invokes a specified function for each {@link QuadtreeTile} that was rendered
 * in the most recent frame.
 *
 * @param {Function} tileFunction The function to invoke for each rendered tile.  The
 *        function is passed a reference to the tile as its only parameter.
 */
QuadtreePrimitive.prototype.forEachRenderedTile = function (tileFunction) {
  var tilesRendered = this._tilesToRender;
  for (var i = 0, len = tilesRendered.length; i < len; ++i) {
    tileFunction(tilesRendered[i]);
  }
};

/**
 * Calls the callback when a new tile is rendered that contains the given cartographic. The only parameter
 * is the cartesian position on the tile.
 *
 * @param {Cartographic} cartographic The cartographic position.
 * @param {Function} callback The function to be called when a new tile is loaded containing cartographic.
 * @returns {Function} The function to remove this callback from the quadtree.
 */
QuadtreePrimitive.prototype.updateHeight = function (cartographic, callback) {
  var primitive = this;
  var object = {
    positionOnEllipsoidSurface: undefined,
    positionCartographic: cartographic,
    level: -1,
    callback: callback,
  };

  object.removeFunc = function () {
    var addedCallbacks = primitive._addHeightCallbacks;
    var length = addedCallbacks.length;
    for (var i = 0; i < length; ++i) {
      if (addedCallbacks[i] === object) {
        addedCallbacks.splice(i, 1);
        break;
      }
    }
    primitive._removeHeightCallbacks.push(object);
  };

  primitive._addHeightCallbacks.push(object);
  return object.removeFunc;
};

/**
 * Updates the tile provider imagery and continues to process the tile load queue.
 * @private
 */
QuadtreePrimitive.prototype.update = function (frameState) {
  if (defined(this._tileProvider.update)) {
    this._tileProvider.update(frameState);
  }
};

function clearTileLoadQueue(primitive) {
  var debug = primitive._debug;
  debug.maxDepth = 0;
  debug.maxDepthVisited = 0;
  debug.tilesVisited = 0;
  debug.tilesCulled = 0;
  debug.tilesRendered = 0;
  debug.tilesWaitingForChildren = 0;

  primitive._tileLoadQueueHigh.length = 0;
  primitive._tileLoadQueueMedium.length = 0;
  primitive._tileLoadQueueLow.length = 0;
}

/**
 * Initializes values for a new render frame and prepare the tile load queue.
 * @private
 */
QuadtreePrimitive.prototype.beginFrame = function (frameState) {
  var passes = frameState.passes;
  if (!passes.render) {
    return;
  }

  if (this._tilesInvalidated) {
    invalidateAllTiles(this);
    this._tilesInvalidated = false;
  }

  // Gets commands for any texture re-projections
  this._tileProvider.initialize(frameState);

  clearTileLoadQueue(this);

  if (this._debug.suspendLodUpdate) {
    return;
  }

  this._tileReplacementQueue.markStartOfRenderFrame();
};

/**
 * Selects new tiles to load based on the frame state and creates render commands.
 * @private
 */
QuadtreePrimitive.prototype.render = function (frameState) {
  var passes = frameState.passes;
  var tileProvider = this._tileProvider;

  if (passes.render) {
    tileProvider.beginUpdate(frameState);

    selectTilesForRendering(this, frameState);
    createRenderCommandsForSelectedTiles(this, frameState);

    tileProvider.endUpdate(frameState);
  }

  if (passes.pick && this._tilesToRender.length > 0) {
    tileProvider.updateForPick(frameState);
  }
};

/**
 * Checks if the load queue length has changed since the last time we raised a queue change event - if so, raises
 * a new change event at the end of the render cycle.
 */
function updateTileLoadProgress(primitive, frameState) {
  var currentLoadQueueLength =
    primitive._tileLoadQueueHigh.length +
    primitive._tileLoadQueueMedium.length +
    primitive._tileLoadQueueLow.length;

  if (
    currentLoadQueueLength !== primitive._lastTileLoadQueueLength ||
    primitive._tilesInvalidated
  ) {
    frameState.afterRender.push(
      Event.prototype.raiseEvent.bind(
        primitive._tileLoadProgressEvent,
        currentLoadQueueLength
      )
    );
    primitive._lastTileLoadQueueLength = currentLoadQueueLength;
  }

  var debug = primitive._debug;
  if (debug.enableDebugOutput && !debug.suspendLodUpdate) {
    debug.maxDepth = primitive._tilesToRender.reduce(function (max, tile) {
      return Math.max(max, tile.level);
    }, -1);
    debug.tilesRendered = primitive._tilesToRender.length;

    if (
      debug.tilesVisited !== debug.lastTilesVisited ||
      debug.tilesRendered !== debug.lastTilesRendered ||
      debug.tilesCulled !== debug.lastTilesCulled ||
      debug.maxDepth !== debug.lastMaxDepth ||
      debug.tilesWaitingForChildren !== debug.lastTilesWaitingForChildren ||
      debug.maxDepthVisited !== debug.lastMaxDepthVisited
    ) {
      console.log(
        "Visited " +
          debug.tilesVisited +
          ", Rendered: " +
          debug.tilesRendered +
          ", Culled: " +
          debug.tilesCulled +
          ", Max Depth Rendered: " +
          debug.maxDepth +
          ", Max Depth Visited: " +
          debug.maxDepthVisited +
          ", Waiting for children: " +
          debug.tilesWaitingForChildren
      );

      debug.lastTilesVisited = debug.tilesVisited;
      debug.lastTilesRendered = debug.tilesRendered;
      debug.lastTilesCulled = debug.tilesCulled;
      debug.lastMaxDepth = debug.maxDepth;
      debug.lastTilesWaitingForChildren = debug.tilesWaitingForChildren;
      debug.lastMaxDepthVisited = debug.maxDepthVisited;
    }
  }
}

/**
 * Updates terrain heights.
 * @private
 */
QuadtreePrimitive.prototype.endFrame = function (frameState) {
  var passes = frameState.passes;
  if (!passes.render || frameState.mode === SceneMode.MORPHING) {
    // Only process the load queue for a single pass.
    // Don't process the load queue or update heights during the morph flights.
    return;
  }

  // Load/create resources for terrain and imagery. Prepare texture re-projections for the next frame.
  processTileLoadQueue(this, frameState);
  updateHeights(this, frameState);
  updateTileLoadProgress(this, frameState);
};

/**
 * Returns true if this object was destroyed; otherwise, false.
 * <br /><br />
 * If this object was destroyed, it should not be used; calling any function other than
 * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.
 *
 * @memberof QuadtreePrimitive
 *
 * @returns {Boolean} True if this object was destroyed; otherwise, false.
 *
 * @see QuadtreePrimitive#destroy
 */
QuadtreePrimitive.prototype.isDestroyed = function () {
  return false;
};

/**
 * Destroys the WebGL resources held by this object.  Destroying an object allows for deterministic
 * release of WebGL resources, instead of relying on the garbage collector to destroy this object.
 * <br /><br />
 * Once an object is destroyed, it should not be used; calling any function other than
 * <code>isDestroyed</code> will result in a {@link DeveloperError} exception.  Therefore,
 * assign the return value (<code>undefined</code>) to the object as done in the example.
 *
 * @memberof QuadtreePrimitive
 *
 * @exception {DeveloperError} This object was destroyed, i.e., destroy() was called.
 *
 *
 * @example
 * primitive = primitive && primitive.destroy();
 *
 * @see QuadtreePrimitive#isDestroyed
 */
QuadtreePrimitive.prototype.destroy = function () {
  this._tileProvider = this._tileProvider && this._tileProvider.destroy();
};

var comparisonPoint;
var centerScratch = new Cartographic();
function compareDistanceToPoint(a, b) {
  var center = Rectangle.center(a.rectangle, centerScratch);
  var alon = center.longitude - comparisonPoint.longitude;
  var alat = center.latitude - comparisonPoint.latitude;

  center = Rectangle.center(b.rectangle, centerScratch);
  var blon = center.longitude - comparisonPoint.longitude;
  var blat = center.latitude - comparisonPoint.latitude;

  return alon * alon + alat * alat - (blon * blon + blat * blat);
}

var cameraOriginScratch = new Cartesian3();
var rootTraversalDetails = [];

function selectTilesForRendering(primitive, frameState) {
  var debug = primitive._debug;
  if (debug.suspendLodUpdate) {
    return;
  }

  // Clear the render list.
  var tilesToRender = primitive._tilesToRender;
  tilesToRender.length = 0;

  // We can't render anything before the level zero tiles exist.
  var i;
  var tileProvider = primitive._tileProvider;
  if (!defined(primitive._levelZeroTiles)) {
    if (tileProvider.ready) {
      var tilingScheme = tileProvider.tilingScheme;
      primitive._levelZeroTiles = QuadtreeTile.createLevelZeroTiles(
        tilingScheme
      );
      var numberOfRootTiles = primitive._levelZeroTiles.length;
      if (rootTraversalDetails.length < numberOfRootTiles) {
        rootTraversalDetails = new Array(numberOfRootTiles);
        for (i = 0; i < numberOfRootTiles; ++i) {
          if (rootTraversalDetails[i] === undefined) {
            rootTraversalDetails[i] = new TraversalDetails();
          }
        }
      }
    } else {
      // Nothing to do until the provider is ready.
      return;
    }
  }

  primitive._occluders.ellipsoid.cameraPosition = frameState.camera.positionWC;

  var tile;
  var levelZeroTiles = primitive._levelZeroTiles;
  var occluders = levelZeroTiles.length > 1 ? primitive._occluders : undefined;

  // Sort the level zero tiles by the distance from the center to the camera.
  // The level zero tiles aren't necessarily a nice neat quad, so we can't use the
  // quadtree ordering we use elsewhere in the tree
  comparisonPoint = frameState.camera.positionCartographic;
  levelZeroTiles.sort(compareDistanceToPoint);

  var customDataAdded = primitive._addHeightCallbacks;
  var customDataRemoved = primitive._removeHeightCallbacks;
  var frameNumber = frameState.frameNumber;

  var len;
  if (customDataAdded.length > 0 || customDataRemoved.length > 0) {
    for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
      tile = levelZeroTiles[i];
      tile._updateCustomData(frameNumber, customDataAdded, customDataRemoved);
    }

    customDataAdded.length = 0;
    customDataRemoved.length = 0;
  }

  var camera = frameState.camera;

  primitive._cameraPositionCartographic = camera.positionCartographic;
  var cameraFrameOrigin = Matrix4.getTranslation(
    camera.transform,
    cameraOriginScratch
  );
  primitive._cameraReferenceFrameOriginCartographic = primitive.tileProvider.tilingScheme.ellipsoid.cartesianToCartographic(
    cameraFrameOrigin,
    primitive._cameraReferenceFrameOriginCartographic
  );

  // Traverse in depth-first, near-to-far order.
  for (i = 0, len = levelZeroTiles.length; i < len; ++i) {
    tile = levelZeroTiles[i];
    primitive._tileReplacementQueue.markTileRendered(tile);
    if (!tile.renderable) {
      queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState);
      ++debug.tilesWaitingForChildren;
    } else {
      visitIfVisible(
        primitive,
        tile,
        tileProvider,
        frameState,
        occluders,
        false,
        rootTraversalDetails[i]
      );
    }
  }

  primitive._lastSelectionFrameNumber = frameNumber;
}

function queueTileLoad(primitive, queue, tile, frameState) {
  if (!tile.needsLoading) {
    return;
  }

  if (primitive.tileProvider.computeTileLoadPriority !== undefined) {
    tile._loadPriority = primitive.tileProvider.computeTileLoadPriority(
      tile,
      frameState
    );
  }
  queue.push(tile);
}

/**
 * Tracks details of traversing a tile while selecting tiles for rendering.
 * @alias TraversalDetails
 * @constructor
 * @private
 */
function TraversalDetails() {
  /**
   * True if all selected (i.e. not culled or refined) tiles in this tile's subtree
   * are renderable. If the subtree is renderable, we'll render it; no drama.
   */
  this.allAreRenderable = true;

  /**
   * True if any tiles in this tile's subtree were rendered last frame. If any
   * were, we must render the subtree rather than this tile, because rendering
   * this tile would cause detail to vanish that was visible last frame, and
   * that's no good.
   */
  this.anyWereRenderedLastFrame = false;

  /**
   * Counts the number of selected tiles in this tile's subtree that are
   * not yet ready to be rendered because they need more loading. Note that
   * this value will _not_ necessarily be zero when
   * {@link TraversalDetails#allAreRenderable} is true, for subtle reasons.
   * When {@link TraversalDetails#allAreRenderable} and
   * {@link TraversalDetails#anyWereRenderedLastFrame} are both false, we
   * will render this tile instead of any tiles in its subtree and
   * the `allAreRenderable` value for this tile will reflect only whether _this_
   * tile is renderable. The `notYetRenderableCount` value, however, will still
   * reflect the total number of tiles that we are waiting on, including the
   * ones that we're not rendering. `notYetRenderableCount` is only reset
   * when a subtree is removed from the render queue because the
   * `notYetRenderableCount` exceeds the
   * {@link QuadtreePrimitive#loadingDescendantLimit}.
   */
  this.notYetRenderableCount = 0;
}

function TraversalQuadDetails() {
  this.southwest = new TraversalDetails();
  this.southeast = new TraversalDetails();
  this.northwest = new TraversalDetails();
  this.northeast = new TraversalDetails();
}

TraversalQuadDetails.prototype.combine = function (result) {
  var southwest = this.southwest;
  var southeast = this.southeast;
  var northwest = this.northwest;
  var northeast = this.northeast;

  result.allAreRenderable =
    southwest.allAreRenderable &&
    southeast.allAreRenderable &&
    northwest.allAreRenderable &&
    northeast.allAreRenderable;
  result.anyWereRenderedLastFrame =
    southwest.anyWereRenderedLastFrame ||
    southeast.anyWereRenderedLastFrame ||
    northwest.anyWereRenderedLastFrame ||
    northeast.anyWereRenderedLastFrame;
  result.notYetRenderableCount =
    southwest.notYetRenderableCount +
    southeast.notYetRenderableCount +
    northwest.notYetRenderableCount +
    northeast.notYetRenderableCount;
};

var traversalQuadsByLevel = new Array(31); // level 30 tiles are ~2cm wide at the equator, should be good enough.
for (var i = 0; i < traversalQuadsByLevel.length; ++i) {
  traversalQuadsByLevel[i] = new TraversalQuadDetails();
}

/**
 * Visits a tile for possible rendering. When we call this function with a tile:
 *
 *    * the tile has been determined to be visible (possibly based on a bounding volume that is not very tight-fitting)
 *    * its parent tile does _not_ meet the SSE (unless ancestorMeetsSse=true, see comments below)
 *    * the tile may or may not be renderable
 *
 * @private
 *
 * @param {Primitive} primitive The QuadtreePrimitive.
 * @param {FrameState} frameState The frame state.
 * @param {QuadtreeTile} tile The tile to visit
 * @param {Boolean} ancestorMeetsSse True if a tile higher in the tile tree already met the SSE and we're refining further only
 *                  to maintain detail while that higher tile loads.
 * @param {TraversalDetails} traveralDetails On return, populated with details of how the traversal of this tile went.
 */
function visitTile(
  primitive,
  frameState,
  tile,
  ancestorMeetsSse,
  traversalDetails
) {
  var debug = primitive._debug;

  ++debug.tilesVisited;

  primitive._tileReplacementQueue.markTileRendered(tile);
  tile._updateCustomData(frameState.frameNumber);

  if (tile.level > debug.maxDepthVisited) {
    debug.maxDepthVisited = tile.level;
  }

  var meetsSse =
    screenSpaceError(primitive, frameState, tile) <
    primitive.maximumScreenSpaceError;

  var southwestChild = tile.southwestChild;
  var southeastChild = tile.southeastChild;
  var northwestChild = tile.northwestChild;
  var northeastChild = tile.northeastChild;

  var lastFrame = primitive._lastSelectionFrameNumber;
  var lastFrameSelectionResult =
    tile._lastSelectionResultFrame === lastFrame
      ? tile._lastSelectionResult
      : TileSelectionResult.NONE;

  var tileProvider = primitive.tileProvider;

  if (meetsSse || ancestorMeetsSse) {
    // This tile (or an ancestor) is the one we want to render this frame, but we'll do different things depending
    // on the state of this tile and on what we did _last_ frame.

    // We can render it if _any_ of the following are true:
    // 1. We rendered it (or kicked it) last frame.
    // 2. This tile was culled last frame, or it wasn't even visited because an ancestor was culled.
    // 3. The tile is completely done loading.
    // 4. a) Terrain is ready, and
    //    b) All necessary imagery is ready. Necessary imagery is imagery that was rendered with this tile
    //       or any descendants last frame. Such imagery is required because rendering this tile without
    //       it would cause detail to disappear.
    //
    // Determining condition 4 is more expensive, so we check the others first.
    //
    // Note that even if we decide to render a tile here, it may later get "kicked" in favor of an ancestor.

    var oneRenderedLastFrame =
      TileSelectionResult.originalResult(lastFrameSelectionResult) ===
      TileSelectionResult.RENDERED;
    var twoCulledOrNotVisited =
      TileSelectionResult.originalResult(lastFrameSelectionResult) ===
        TileSelectionResult.CULLED ||
      lastFrameSelectionResult === TileSelectionResult.NONE;
    var threeCompletelyLoaded = tile.state === QuadtreeTileLoadState.DONE;

    var renderable =
      oneRenderedLastFrame || twoCulledOrNotVisited || threeCompletelyLoaded;

    if (!renderable) {
      // Check the more expensive condition 4 above. This requires details of the thing
      // we're rendering (e.g. the globe surface), so delegate it to the tile provider.
      if (defined(tileProvider.canRenderWithoutLosingDetail)) {
        renderable = tileProvider.canRenderWithoutLosingDetail(tile);
      }
    }

    if (renderable) {
      // Only load this tile if it (not just an ancestor) meets the SSE.
      if (meetsSse) {
        queueTileLoad(
          primitive,
          primitive._tileLoadQueueMedium,
          tile,
          frameState
        );
      }
      addTileToRenderList(primitive, tile);

      traversalDetails.allAreRenderable = tile.renderable;
      traversalDetails.anyWereRenderedLastFrame =
        lastFrameSelectionResult === TileSelectionResult.RENDERED;
      traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1;

      tile._lastSelectionResultFrame = frameState.frameNumber;
      tile._lastSelectionResult = TileSelectionResult.RENDERED;

      if (!traversalDetails.anyWereRenderedLastFrame) {
        // Tile is newly-rendered this frame, so update its heights.
        primitive._tileToUpdateHeights.push(tile);
      }

      return;
    }

    // Otherwise, we can't render this tile (or its fill) because doing so would cause detail to disappear
    // that was visible last frame. Instead, keep rendering any still-visible descendants that were rendered
    // last frame and render fills for newly-visible descendants. E.g. if we were rendering level 15 last
    // frame but this frame we want level 14 and the closest renderable level <= 14 is 0, rendering level
    // zero would be pretty jarring so instead we keep rendering level 15 even though its SSE is better
    // than required. So fall through to continue traversal...
    ancestorMeetsSse = true;

    // Load this blocker tile with high priority, but only if this tile (not just an ancestor) meets the SSE.
    if (meetsSse) {
      queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState);
    }
  }

  if (tileProvider.canRefine(tile)) {
    var allAreUpsampled =
      southwestChild.upsampledFromParent &&
      southeastChild.upsampledFromParent &&
      northwestChild.upsampledFromParent &&
      northeastChild.upsampledFromParent;

    if (allAreUpsampled) {
      // No point in rendering the children because they're all upsampled.  Render this tile instead.
      addTileToRenderList(primitive, tile);

      // Rendered tile that's not waiting on children loads with medium priority.
      queueTileLoad(
        primitive,
        primitive._tileLoadQueueMedium,
        tile,
        frameState
      );

      // Make sure we don't unload the children and forget they're upsampled.
      primitive._tileReplacementQueue.markTileRendered(southwestChild);
      primitive._tileReplacementQueue.markTileRendered(southeastChild);
      primitive._tileReplacementQueue.markTileRendered(northwestChild);
      primitive._tileReplacementQueue.markTileRendered(northeastChild);

      traversalDetails.allAreRenderable = tile.renderable;
      traversalDetails.anyWereRenderedLastFrame =
        lastFrameSelectionResult === TileSelectionResult.RENDERED;
      traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1;

      tile._lastSelectionResultFrame = frameState.frameNumber;
      tile._lastSelectionResult = TileSelectionResult.RENDERED;

      if (!traversalDetails.anyWereRenderedLastFrame) {
        // Tile is newly-rendered this frame, so update its heights.
        primitive._tileToUpdateHeights.push(tile);
      }

      return;
    }

    // SSE is not good enough, so refine.
    tile._lastSelectionResultFrame = frameState.frameNumber;
    tile._lastSelectionResult = TileSelectionResult.REFINED;

    var firstRenderedDescendantIndex = primitive._tilesToRender.length;
    var loadIndexLow = primitive._tileLoadQueueLow.length;
    var loadIndexMedium = primitive._tileLoadQueueMedium.length;
    var loadIndexHigh = primitive._tileLoadQueueHigh.length;
    var tilesToUpdateHeightsIndex = primitive._tileToUpdateHeights.length;

    // No need to add the children to the load queue because they'll be added (if necessary) when they're visited.
    visitVisibleChildrenNearToFar(
      primitive,
      southwestChild,
      southeastChild,
      northwestChild,
      northeastChild,
      frameState,
      ancestorMeetsSse,
      traversalDetails
    );

    // If no descendant tiles were added to the render list by the function above, it means they were all
    // culled even though this tile was deemed visible. That's pretty common.

    if (firstRenderedDescendantIndex !== primitive._tilesToRender.length) {
      // At least one descendant tile was added to the render list.
      // The traversalDetails tell us what happened while visiting the children.

      var allAreRenderable = traversalDetails.allAreRenderable;
      var anyWereRenderedLastFrame = traversalDetails.anyWereRenderedLastFrame;
      var notYetRenderableCount = traversalDetails.notYetRenderableCount;
      var queuedForLoad = false;

      if (!allAreRenderable && !anyWereRenderedLastFrame) {
        // Some of our descendants aren't ready to render yet, and none were rendered last frame,
        // so kick them all out of the render list and render this tile instead. Continue to load them though!

        // Mark the rendered descendants and their ancestors - up to this tile - as kicked.
        var renderList = primitive._tilesToRender;
        for (var i = firstRenderedDescendantIndex; i < renderList.length; ++i) {
          var workTile = renderList[i];
          while (
            workTile !== undefined &&
            workTile._lastSelectionResult !== TileSelectionResult.KICKED &&
            workTile !== tile
          ) {
            workTile._lastSelectionResult = TileSelectionResult.kick(
              workTile._lastSelectionResult
            );
            workTile = workTile.parent;
          }
        }

        // Remove all descendants from the render list and add this tile.
        primitive._tilesToRender.length = firstRenderedDescendantIndex;
        primitive._tileToUpdateHeights.length = tilesToUpdateHeightsIndex;
        addTileToRenderList(primitive, tile);

        tile._lastSelectionResult = TileSelectionResult.RENDERED;

        // If we're waiting on heaps of descendants, the above will take too long. So in that case,
        // load this tile INSTEAD of loading any of the descendants, and tell the up-level we're only waiting
        // on this tile. Keep doing this until we actually manage to render this tile.
        var wasRenderedLastFrame =
          lastFrameSelectionResult === TileSelectionResult.RENDERED;
        if (
          !wasRenderedLastFrame &&
          notYetRenderableCount > primitive.loadingDescendantLimit
        ) {
          // Remove all descendants from the load queues.
          primitive._tileLoadQueueLow.length = loadIndexLow;
          primitive._tileLoadQueueMedium.length = loadIndexMedium;
          primitive._tileLoadQueueHigh.length = loadIndexHigh;
          queueTileLoad(
            primitive,
            primitive._tileLoadQueueMedium,
            tile,
            frameState
          );
          traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1;
          queuedForLoad = true;
        }

        traversalDetails.allAreRenderable = tile.renderable;
        traversalDetails.anyWereRenderedLastFrame = wasRenderedLastFrame;

        if (!wasRenderedLastFrame) {
          // Tile is newly-rendered this frame, so update its heights.
          primitive._tileToUpdateHeights.push(tile);
        }

        ++debug.tilesWaitingForChildren;
      }

      if (primitive.preloadAncestors && !queuedForLoad) {
        queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState);
      }
    }

    return;
  }

  tile._lastSelectionResultFrame = frameState.frameNumber;
  tile._lastSelectionResult = TileSelectionResult.RENDERED;

  // We'd like to refine but can't because we have no availability data for this tile's children,
  // so we have no idea if refinining would involve a load or an upsample. We'll have to finish
  // loading this tile first in order to find that out, so load this refinement blocker with
  // high priority.
  addTileToRenderList(primitive, tile);
  queueTileLoad(primitive, primitive._tileLoadQueueHigh, tile, frameState);

  traversalDetails.allAreRenderable = tile.renderable;
  traversalDetails.anyWereRenderedLastFrame =
    lastFrameSelectionResult === TileSelectionResult.RENDERED;
  traversalDetails.notYetRenderableCount = tile.renderable ? 0 : 1;
}

function visitVisibleChildrenNearToFar(
  primitive,
  southwest,
  southeast,
  northwest,
  northeast,
  frameState,
  ancestorMeetsSse,
  traversalDetails
) {
  var cameraPosition = frameState.camera.positionCartographic;
  var tileProvider = primitive._tileProvider;
  var occluders = primitive._occluders;

  var quadDetails = traversalQuadsByLevel[southwest.level];
  var southwestDetails = quadDetails.southwest;
  var southeastDetails = quadDetails.southeast;
  var northwestDetails = quadDetails.northwest;
  var northeastDetails = quadDetails.northeast;

  if (cameraPosition.longitude < southwest.rectangle.east) {
    if (cameraPosition.latitude < southwest.rectangle.north) {
      // Camera in southwest quadrant
      visitIfVisible(
        primitive,
        southwest,
        tileProvider,
        frameState,
        occluders,
        ancestorMeetsSse,
        southwestDetails
      );
      visitIfVisible(
        primitive,
        southeast,
        tileProvider,
        frameState,
        occluders,
        ancestorMeetsSse,
        southeastDetails
      );
      visitIfVisible(
        primitive,
        northwest,
        tileProvider,
        frameState,
        occluders,
        ancestorMeetsSse,
        northwestDetails
      );
      visitIfVisible(
        primitive,
        northeast,
        tileProvider,
        frameState,
        occluders,
        ancestorMeetsSse,
        northeastDetails
      );
    } else {
      // Camera in northwest quadrant
      visitIfVisible(
        primitive,
        northwest,
        tileProvider,
        frameState,
        occluders,
        ancestorMeetsSse,
        northwestDetails
      );
      visitIfVisible(
        primitive,
        southwest,
        tileProvider,
        frameState,
        occluders,
        ancestorMeetsSse,
        southwestDetails
      );
      visitIfVisible(
        primitive,
        northeast,
        tileProvider,
        frameState,
        occluders,
        ancestorMeetsSse,
        northeastDetails
      );
      visitIfVisible(
        primitive,
        southeast,
        tileProvider,
        frameState,
        occluders,
        ancestorMeetsSse,
        southeastDetails
      );
    }
  } else if (cameraPosition.latitude < southwest.rectangle.north) {
    // Camera southeast quadrant
    visitIfVisible(
      primitive,
      southeast,
      tileProvider,
      frameState,
      occluders,
      ancestorMeetsSse,
      southeastDetails
    );
    visitIfVisible(
      primitive,
      southwest,
      tileProvider,
      frameState,
      occluders,
      ancestorMeetsSse,
      southwestDetails
    );
    visitIfVisible(
      primitive,
      northeast,
      tileProvider,
      frameState,
      occluders,
      ancestorMeetsSse,
      northeastDetails
    );
    visitIfVisible(
      primitive,
      northwest,
      tileProvider,
      frameState,
      occluders,
      ancestorMeetsSse,
      northwestDetails
    );
  } else {
    // Camera in northeast quadrant
    visitIfVisible(
      primitive,
      northeast,
      tileProvider,
      frameState,
      occluders,
      ancestorMeetsSse,
      northeastDetails
    );
    visitIfVisible(
      primitive,
      northwest,
      tileProvider,
      frameState,
      occluders,
      ancestorMeetsSse,
      northwestDetails
    );
    visitIfVisible(
      primitive,
      southeast,
      tileProvider,
      frameState,
      occluders,
      ancestorMeetsSse,
      southeastDetails
    );
    visitIfVisible(
      primitive,
      southwest,
      tileProvider,
      frameState,
      occluders,
      ancestorMeetsSse,
      southwestDetails
    );
  }

  quadDetails.combine(traversalDetails);
}

function containsNeededPosition(primitive, tile) {
  var rectangle = tile.rectangle;
  return (
    (defined(primitive._cameraPositionCartographic) &&
      Rectangle.contains(rectangle, primitive._cameraPositionCartographic)) ||
    (defined(primitive._cameraReferenceFrameOriginCartographic) &&
      Rectangle.contains(
        rectangle,
        primitive._cameraReferenceFrameOriginCartographic
      ))
  );
}

function visitIfVisible(
  primitive,
  tile,
  tileProvider,
  frameState,
  occluders,
  ancestorMeetsSse,
  traversalDetails
) {
  if (
    tileProvider.computeTileVisibility(tile, frameState, occluders) !==
    Visibility.NONE
  ) {
    return visitTile(
      primitive,
      frameState,
      tile,
      ancestorMeetsSse,
      traversalDetails
    );
  }

  ++primitive._debug.tilesCulled;
  primitive._tileReplacementQueue.markTileRendered(tile);

  traversalDetails.allAreRenderable = true;
  traversalDetails.anyWereRenderedLastFrame = false;
  traversalDetails.notYetRenderableCount = 0;

  if (containsNeededPosition(primitive, tile)) {
    // Load the tile(s) that contains the camera's position and
    // the origin of its reference frame with medium priority.
    // But we only need to load until the terrain is available, no need to load imagery.
    if (!defined(tile.data) || !defined(tile.data.vertexArray)) {
      queueTileLoad(
        primitive,
        primitive._tileLoadQueueMedium,
        tile,
        frameState
      );
    }

    var lastFrame = primitive._lastSelectionFrameNumber;
    var lastFrameSelectionResult =
      tile._lastSelectionResultFrame === lastFrame
        ? tile._lastSelectionResult
        : TileSelectionResult.NONE;
    if (
      lastFrameSelectionResult !== TileSelectionResult.CULLED_BUT_NEEDED &&
      lastFrameSelectionResult !== TileSelectionResult.RENDERED
    ) {
      primitive._tileToUpdateHeights.push(tile);
    }

    tile._lastSelectionResult = TileSelectionResult.CULLED_BUT_NEEDED;
  } else if (primitive.preloadSiblings || tile.level === 0) {
    // Load culled level zero tiles with low priority.
    // For all other levels, only load culled tiles if preloadSiblings is enabled.
    queueTileLoad(primitive, primitive._tileLoadQueueLow, tile, frameState);
    tile._lastSelectionResult = TileSelectionResult.CULLED;
  } else {
    tile._lastSelectionResult = TileSelectionResult.CULLED;
  }

  tile._lastSelectionResultFrame = frameState.frameNumber;
}

function screenSpaceError(primitive, frameState, tile) {
  if (
    frameState.mode === SceneMode.SCENE2D ||
    frameState.camera.frustum instanceof OrthographicFrustum ||
    frameState.camera.frustum instanceof OrthographicOffCenterFrustum
  ) {
    return screenSpaceError2D(primitive, frameState, tile);
  }

  var maxGeometricError = primitive._tileProvider.getLevelMaximumGeometricError(
    tile.level
  );

  var distance = tile._distance;
  var height = frameState.context.drawingBufferHeight;
  var sseDenominator = frameState.camera.frustum.sseDenominator;

  var error = (maxGeometricError * height) / (distance * sseDenominator);

  if (frameState.fog.enabled) {
    error -=
      CesiumMath.fog(distance, frameState.fog.density) * frameState.fog.sse;
  }

  error /= frameState.pixelRatio;

  return error;
}

function screenSpaceError2D(primitive, frameState, tile) {
  var camera = frameState.camera;
  var frustum = camera.frustum;
  if (defined(frustum._offCenterFrustum)) {
    frustum = frustum._offCenterFrustum;
  }

  var context = frameState.context;
  var width = context.drawingBufferWidth;
  var height = context.drawingBufferHeight;

  var maxGeometricError = primitive._tileProvider.getLevelMaximumGeometricError(
    tile.level
  );
  var pixelSize =
    Math.max(frustum.top - frustum.bottom, frustum.right - frustum.left) /
    Math.max(width, height);
  var error = maxGeometricError / pixelSize;

  if (frameState.fog.enabled && frameState.mode !== SceneMode.SCENE2D) {
    error -=
      CesiumMath.fog(tile._distance, frameState.fog.density) *
      frameState.fog.sse;
  }

  error /= frameState.pixelRatio;

  return error;
}

function addTileToRenderList(primitive, tile) {
  primitive._tilesToRender.push(tile);
}

function processTileLoadQueue(primitive, frameState) {
  var tileLoadQueueHigh = primitive._tileLoadQueueHigh;
  var tileLoadQueueMedium = primitive._tileLoadQueueMedium;
  var tileLoadQueueLow = primitive._tileLoadQueueLow;

  if (
    tileLoadQueueHigh.length === 0 &&
    tileLoadQueueMedium.length === 0 &&
    tileLoadQueueLow.length === 0
  ) {
    return;
  }

  // Remove any tiles that were not used this frame beyond the number
  // we're allowed to keep.
  primitive._tileReplacementQueue.trimTiles(primitive.tileCacheSize);

  var endTime = getTimestamp() + primitive._loadQueueTimeSlice;
  var tileProvider = primitive._tileProvider;

  var didSomeLoading = processSinglePriorityLoadQueue(
    primitive,
    frameState,
    tileProvider,
    endTime,
    tileLoadQueueHigh,
    false
  );
  didSomeLoading = processSinglePriorityLoadQueue(
    primitive,
    frameState,
    tileProvider,
    endTime,
    tileLoadQueueMedium,
    didSomeLoading
  );
  processSinglePriorityLoadQueue(
    primitive,
    frameState,
    tileProvider,
    endTime,
    tileLoadQueueLow,
    didSomeLoading
  );
}

function sortByLoadPriority(a, b) {
  return a._loadPriority - b._loadPriority;
}

function processSinglePriorityLoadQueue(
  primitive,
  frameState,
  tileProvider,
  endTime,
  loadQueue,
  didSomeLoading
) {
  if (tileProvider.computeTileLoadPriority !== undefined) {
    loadQueue.sort(sortByLoadPriority);
  }

  for (
    var i = 0, len = loadQueue.length;
    i < len && (getTimestamp() < endTime || !didSomeLoading);
    ++i
  ) {
    var tile = loadQueue[i];
    primitive._tileReplacementQueue.markTileRendered(tile);
    tileProvider.loadTile(frameState, tile);
    didSomeLoading = true;
  }

  return didSomeLoading;
}

var scratchRay = new Ray();
var scratchCartographic = new Cartographic();
var scratchPosition = new Cartesian3();
var scratchArray = [];

function updateHeights(primitive, frameState) {
  if (!primitive.tileProvider.ready) {
    return;
  }

  var tryNextFrame = scratchArray;
  tryNextFrame.length = 0;
  var tilesToUpdateHeights = primitive._tileToUpdateHeights;
  var terrainProvider = primitive._tileProvider.terrainProvider;

  var startTime = getTimestamp();
  var timeSlice = primitive._updateHeightsTimeSlice;
  var endTime = startTime + timeSlice;

  var mode = frameState.mode;
  var projection = frameState.mapProjection;
  var ellipsoid = primitive.tileProvider.tilingScheme.ellipsoid;
  var i;

  while (tilesToUpdateHeights.length > 0) {
    var tile = tilesToUpdateHeights[0];
    if (!defined(tile.data) || !defined(tile.data.mesh)) {
      // Tile isn't loaded enough yet, so try again next frame if this tile is still
      // being rendered.
      var selectionResult =
        tile._lastSelectionResultFrame === primitive._lastSelectionFrameNumber
          ? tile._lastSelectionResult
          : TileSelectionResult.NONE;
      if (
        selectionResult === TileSelectionResult.RENDERED ||
        selectionResult === TileSelectionResult.CULLED_BUT_NEEDED
      ) {
        tryNextFrame.push(tile);
      }
      tilesToUpdateHeights.shift();
      primitive._lastTileIndex = 0;
      continue;
    }
    var customData = tile.customData;
    var customDataLength = customData.length;

    var timeSliceMax = false;
    for (i = primitive._lastTileIndex; i < customDataLength; ++i) {
      var data = customData[i];

      if (tile.level > data.level) {
        if (!defined(data.positionOnEllipsoidSurface)) {
          // cartesian has to be on the ellipsoid surface for `ellipsoid.geodeticSurfaceNormal`
          data.positionOnEllipsoidSurface = Cartesian3.fromRadians(
            data.positionCartographic.longitude,
            data.positionCartographic.latitude,
            0.0,
            ellipsoid
          );
        }

        if (mode === SceneMode.SCENE3D) {
          var surfaceNormal = ellipsoid.geodeticSurfaceNormal(
            data.positionOnEllipsoidSurface,
            scratchRay.direction
          );

          // compute origin point

          // Try to find the intersection point between the surface normal and z-axis.
          // minimum height (-11500.0) for the terrain set, need to get this information from the terrain provider
          var rayOrigin = ellipsoid.getSurfaceNormalIntersectionWithZAxis(
            data.positionOnEllipsoidSurface,
            11500.0,
            scratchRay.origin
          );

          // Theoretically, not with Earth datums, the intersection point can be outside the ellipsoid
          if (!defined(rayOrigin)) {
            // intersection point is outside the ellipsoid, try other value
            // minimum height (-11500.0) for the terrain set, need to get this information from the terrain provider
            var minimumHeight;
            if (defined(tile.data.tileBoundingRegion)) {
              minimumHeight = tile.data.tileBoundingRegion.minimumHeight;
            }
            var magnitude = Math.min(
              defaultValue(minimumHeight, 0.0),
              -11500.0
            );

            // multiply by the *positive* value of the magnitude
            var vectorToMinimumPoint = Cartesian3.multiplyByScalar(
              surfaceNormal,
              Math.abs(magnitude) + 1,
              scratchPosition
            );
            Cartesian3.subtract(
              data.positionOnEllipsoidSurface,
              vectorToMinimumPoint,
              scratchRay.origin
            );
          }
        } else {
          Cartographic.clone(data.positionCartographic, scratchCartographic);

          // minimum height for the terrain set, need to get this information from the terrain provider
          scratchCartographic.height = -11500.0;
          projection.project(scratchCartographic, scratchPosition);
          Cartesian3.fromElements(
            scratchPosition.z,
            scratchPosition.x,
            scratchPosition.y,
            scratchPosition
          );
          Cartesian3.clone(scratchPosition, scratchRay.origin);
          Cartesian3.clone(Cartesian3.UNIT_X, scratchRay.direction);
        }

        var position = tile.data.pick(
          scratchRay,
          mode,
          projection,
          false,
          scratchPosition
        );
        if (defined(position)) {
          data.callback(position);
          data.level = tile.level;
        }
      } else if (tile.level === data.level) {
        var children = tile.children;
        var childrenLength = children.length;

        var child;
        for (var j = 0; j < childrenLength; ++j) {
          child = children[j];
          if (Rectangle.contains(child.rectangle, data.positionCartographic)) {
            break;
          }
        }

        var tileDataAvailable = terrainProvider.getTileDataAvailable(
          child.x,
          child.y,
          child.level
        );
        var parentTile = tile.parent;
        if (
          (defined(tileDataAvailable) && !tileDataAvailable) ||
          (defined(parentTile) &&
            defined(parentTile.data) &&
            defined(parentTile.data.terrainData) &&
            !parentTile.data.terrainData.isChildAvailable(
              parentTile.x,
              parentTile.y,
              child.x,
              child.y
            ))
        ) {
          data.removeFunc();
        }
      }

      if (getTimestamp() >= endTime) {
        timeSliceMax = true;
        break;
      }
    }

    if (timeSliceMax) {
      primitive._lastTileIndex = i;
      break;
    } else {
      primitive._lastTileIndex = 0;
      tilesToUpdateHeights.shift();
    }
  }
  for (i = 0; i < tryNextFrame.length; i++) {
    tilesToUpdateHeights.push(tryNextFrame[i]);
  }
}

function createRenderCommandsForSelectedTiles(primitive, frameState) {
  var tileProvider = primitive._tileProvider;
  var tilesToRender = primitive._tilesToRender;

  for (var i = 0, len = tilesToRender.length; i < len; ++i) {
    var tile = tilesToRender[i];
    tileProvider.showTileThisFrame(tile, frameState);
  }
}
export default QuadtreePrimitive;
